diff options
Diffstat (limited to 'app/[lng]')
| -rw-r--r-- | app/[lng]/evcp/(evcp)/bqtbe/page.tsx (renamed from app/[lng]/evcp/bqtbe/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/cbe/page.tsx (renamed from app/[lng]/evcp/budgetary/[id]/cbe/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/layout.tsx (renamed from app/[lng]/evcp/budgetary/[id]/layout.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/page.tsx (renamed from app/[lng]/evcp/budgetary/[id]/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/tbe/page.tsx (renamed from app/[lng]/evcp/budgetary/[id]/tbe/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/budgetary-rfq/page.tsx | 86 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/budgetary/[id]/cbe/page.tsx | 56 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/budgetary/[id]/layout.tsx | 80 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/budgetary/[id]/page.tsx | 57 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/budgetary/[id]/tbe/page.tsx | 55 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/budgetary/page.tsx (renamed from app/[lng]/evcp/budgetary/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/equip-class/page.tsx (renamed from app/[lng]/evcp/equip-class/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/faq/manage/actions.ts (renamed from app/[lng]/evcp/faq/manage/actions.ts) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/faq/manage/page.tsx (renamed from app/[lng]/evcp/faq/manage/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/faq/page.tsx (renamed from app/[lng]/evcp/faq/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/form-list/page.tsx (renamed from app/[lng]/evcp/form-list/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/items/page.tsx (renamed from app/[lng]/evcp/items/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/layout.tsx (renamed from app/[lng]/evcp/layout.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/po/page.tsx (renamed from app/[lng]/evcp/po/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/poa/page.tsx (renamed from app/[lng]/evcp/poa/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/pq-criteria/[id]/page.tsx (renamed from app/[lng]/evcp/pq-criteria/[id]/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/pq-criteria/page.tsx (renamed from app/[lng]/evcp/pq-criteria/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/pq/[vendorId]/page.tsx (renamed from app/[lng]/evcp/pq/[vendorId]/page.tsx) | 8 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/pq/page.tsx (renamed from app/[lng]/evcp/pq/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/projects/page.tsx | 75 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/report/page.tsx (renamed from app/[lng]/evcp/report/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/rfq/[id]/cbe/page.tsx (renamed from app/[lng]/evcp/rfq/[id]/cbe/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/rfq/[id]/layout.tsx (renamed from app/[lng]/evcp/rfq/[id]/layout.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/rfq/[id]/page.tsx (renamed from app/[lng]/evcp/rfq/[id]/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/rfq/[id]/tbe/page.tsx (renamed from app/[lng]/evcp/rfq/[id]/tbe/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/rfq/page.tsx (renamed from app/[lng]/evcp/rfq/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/settings/layout.tsx (renamed from app/[lng]/evcp/settings/layout.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/settings/page.tsx (renamed from app/[lng]/evcp/settings/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/settings/preferences/page.tsx (renamed from app/[lng]/evcp/settings/preferences/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/system/admin-users/page.tsx (renamed from app/[lng]/evcp/system/admin-users/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/system/layout.tsx (renamed from app/[lng]/evcp/system/layout.tsx) | 2 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/system/page.tsx (renamed from app/[lng]/evcp/system/page.tsx) | 2 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/system/permissions/page.tsx (renamed from app/[lng]/evcp/system/permissions/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/system/roles/page.tsx (renamed from app/[lng]/evcp/system/roles/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/tag-numbering/page.tsx (renamed from app/[lng]/evcp/tag-numbering/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/tasks/page.tsx (renamed from app/[lng]/evcp/tasks/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/tbe/page.tsx | 113 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/vendor-candidates/page.tsx (renamed from app/[lng]/evcp/vendor-candidates/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/vendor-investigation/page.tsx (renamed from app/[lng]/evcp/vendor-investigation/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/vendors/[id]/info/items/page.tsx (renamed from app/[lng]/evcp/vendors/[id]/info/items/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/vendors/[id]/info/layout.tsx (renamed from app/[lng]/evcp/vendors/[id]/info/layout.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/vendors/[id]/info/page.tsx (renamed from app/[lng]/evcp/vendors/[id]/info/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/vendors/[id]/info/rfq-history/page.tsx (renamed from app/[lng]/evcp/vendors/[id]/info/rfq-history/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/vendors/page.tsx (renamed from app/[lng]/evcp/vendors/page.tsx) | 0 | ||||
| -rw-r--r-- | app/[lng]/evcp/page.tsx | 27 | ||||
| -rw-r--r-- | app/[lng]/partners/pq/page.tsx | 91 |
51 files changed, 572 insertions, 80 deletions
diff --git a/app/[lng]/evcp/bqtbe/page.tsx b/app/[lng]/evcp/(evcp)/bqtbe/page.tsx index 655bd30a..655bd30a 100644 --- a/app/[lng]/evcp/bqtbe/page.tsx +++ b/app/[lng]/evcp/(evcp)/bqtbe/page.tsx diff --git a/app/[lng]/evcp/budgetary/[id]/cbe/page.tsx b/app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/cbe/page.tsx index 9a4ae7eb..9a4ae7eb 100644 --- a/app/[lng]/evcp/budgetary/[id]/cbe/page.tsx +++ b/app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/cbe/page.tsx diff --git a/app/[lng]/evcp/budgetary/[id]/layout.tsx b/app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/layout.tsx index 39f045e5..39f045e5 100644 --- a/app/[lng]/evcp/budgetary/[id]/layout.tsx +++ b/app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/layout.tsx diff --git a/app/[lng]/evcp/budgetary/[id]/page.tsx b/app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/page.tsx index f6160574..f6160574 100644 --- a/app/[lng]/evcp/budgetary/[id]/page.tsx +++ b/app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/page.tsx diff --git a/app/[lng]/evcp/budgetary/[id]/tbe/page.tsx b/app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/tbe/page.tsx index a6259696..a6259696 100644 --- a/app/[lng]/evcp/budgetary/[id]/tbe/page.tsx +++ b/app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/tbe/page.tsx diff --git a/app/[lng]/evcp/(evcp)/budgetary-rfq/page.tsx b/app/[lng]/evcp/(evcp)/budgetary-rfq/page.tsx new file mode 100644 index 00000000..dc2a4a2b --- /dev/null +++ b/app/[lng]/evcp/(evcp)/budgetary-rfq/page.tsx @@ -0,0 +1,86 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { searchParamsCache } from "@/lib/rfqs/validations" +import { getRfqs, getRfqStatusCounts } from "@/lib/rfqs/service" +import { RfqsTable } from "@/lib/rfqs/table/rfqs-table" +import { getAllItems } from "@/lib/items/service" +import { RfqType } from "@/lib/rfqs/validations" +import { Ellipsis } from "lucide-react" + +interface RfqPageProps { + searchParams: Promise<SearchParams>; + rfqType: RfqType; + title: string; + description: string; +} + +export default async function RfqPage({ + searchParams, + rfqType = RfqType.PURCHASE_BUDGETARY, + title = "Budgetary Quote", + description = "Budgetary Quote를 등록하여 요청 및 응답을 관리할 수 있습니다." +}: RfqPageProps) { + const search = searchParamsCache.parse(await searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRfqs({ + ...search, + filters: validFilters, + rfqType // 전달받은 rfqType 사용 + }), + getRfqStatusCounts(rfqType), // rfqType 전달 + getAllItems() + ]) + + return ( + <Shell className="gap-2"> + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center justify-between space-y-2"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + {title} + </h2> + <p className="text-muted-foreground"> + {description} + 기본적인 정보와 RFQ를 위한 아이템 등록 및 첨부를 한 후, + <span className="inline-flex items-center whitespace-nowrap"> + <Ellipsis className="size-3" /> + <span className="ml-1">버튼</span> + </span> 을 클릭하면 "Proceed"를 통해 상세화면으로 이동하여 진행할 수 있습니다. + </p> + </div> + </div> + </div> + + <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> + {/* <DateRangePicker + triggerSize="sm" + triggerClassName="ml-auto w-56 sm:w-60" + align="end" + shallow={false} + /> */} + </React.Suspense> + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={6} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} + shrinkZero + /> + } + > + <RfqsTable promises={promises} rfqType={rfqType} /> + </React.Suspense> + </Shell> + ) +}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/budgetary/[id]/cbe/page.tsx b/app/[lng]/evcp/(evcp)/budgetary/[id]/cbe/page.tsx new file mode 100644 index 00000000..9a4ae7eb --- /dev/null +++ b/app/[lng]/evcp/(evcp)/budgetary/[id]/cbe/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getCBE, getTBE } from "@/lib/rfqs/service" +import { searchParamsCBECache, } from "@/lib/rfqs/validations" +import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" +import { CbeTable } from "@/lib/rfqs/cbe-table/cbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise<SearchParams> +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsCBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getCBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( + <div className="space-y-6"> + <div> + <h3 className="text-lg font-medium"> + Commercial Bid Evaluation + </h3> + <p className="text-sm text-muted-foreground"> + 초대된 벤더에게 CBE를 보낼 수 있습니다. <br/>체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. + </p> + </div> + <Separator /> + <div> + <CbeTable promises={promises} rfqId={idAsNumber}/> + </div> + </div> + ) +}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/budgetary/[id]/layout.tsx b/app/[lng]/evcp/(evcp)/budgetary/[id]/layout.tsx new file mode 100644 index 00000000..39f045e5 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/budgetary/[id]/layout.tsx @@ -0,0 +1,80 @@ +import { Metadata } from "next" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { findVendorById } from "@/lib/vendors/service" // 가정: 여기에 findVendorById가 있다고 가정 +import { Rfq, RfqWithItems } from "@/db/schema/rfq" +import { findRfqById } from "@/lib/rfqs/service" +import { formatDate } from "@/lib/utils" + +export const metadata: Metadata = { + title: "Vendor Detail", +} + +export default async function RfqLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string, id: string } +}) { + + // 1) URL 파라미터에서 id 추출, Number로 변환 + const resolvedParams = await params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + // 2) DB에서 해당 벤더 정보 조회 + const rfq: RfqWithItems | null = await findRfqById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "Matched Vendors", + href: `/${lng}/evcp/budgetary/${id}`, + }, + { + title: "TBE", + href: `/${lng}/evcp/budgetary/${id}/tbe`, + }, + { + title: "CBE", + href: `/${lng}/evcp/budgetary/${id}/cbe`, + }, + + ] + + return ( + <> + <div className="container py-6"> + <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow"> + <div className="hidden space-y-6 p-10 pb-16 md:block"> + <div className="space-y-0.5"> + {/* 4) 벤더 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} + <h2 className="text-2xl font-bold tracking-tight"> + {rfq + ? `${rfq.rfqCode ?? ""} 관리` + : "Loading RFQ..."} + </h2> + + <p className="text-muted-foreground"> + {rfq + ? `${rfq.description ?? ""} ${rfq.lines.map(line => line.itemCode).join(", ")}` + : ""} + </p> + <h3>Due Date:{ rfq && <strong>{formatDate(rfq?.dueDate)}</strong>}</h3> + </div> + <Separator className="my-6" /> + <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0"> + <aside className="-mx-4 lg:w-1/6"> + <SidebarNav items={sidebarNavItems} /> + </aside> + <div className="flex-1">{children}</div> + </div> + </div> + </section> + </div> + </> + ) +}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/budgetary/[id]/page.tsx b/app/[lng]/evcp/(evcp)/budgetary/[id]/page.tsx new file mode 100644 index 00000000..f6160574 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/budgetary/[id]/page.tsx @@ -0,0 +1,57 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getMatchedVendors } from "@/lib/rfqs/service" +import { searchParamsMatchedVCache } from "@/lib/rfqs/validations" +import { MatchedVendorsTable } from "@/lib/rfqs/vendor-table/vendors-table" +import { RfqType } from "@/lib/rfqs/validations" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise<SearchParams> + rfqType: RfqType +} + +export default async function RfqPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + const searchParams = await props.searchParams + const search = searchParamsMatchedVCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getMatchedVendors({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( + <div className="space-y-6"> + <div> + <h3 className="text-lg font-medium"> + Vendors + </h3> + <p className="text-sm text-muted-foreground"> + 등록된 벤더 중에서 이 RFQ 아이템에 매칭되는 업체를 보여줍니다. <br/>"발행하기" 버튼을 통해 RFQ를 전송하면 첨부파일과 함께 RFQ 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. + </p> + </div> + <Separator /> + <div> + <MatchedVendorsTable promises={promises} rfqId={idAsNumber} rfqType={rfqType}/> + </div> + </div> + ) +}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/budgetary/[id]/tbe/page.tsx b/app/[lng]/evcp/(evcp)/budgetary/[id]/tbe/page.tsx new file mode 100644 index 00000000..a6259696 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/budgetary/[id]/tbe/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getTBE } from "@/lib/rfqs/service" +import { searchParamsTBECache } from "@/lib/rfqs/validations" +import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise<SearchParams> +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( + <div className="space-y-6"> + <div> + <h3 className="text-lg font-medium"> + Technical Bid Evaluation + </h3> + <p className="text-sm text-muted-foreground"> + 초대된 벤더에게 TBE를 보낼 수 있습니다. <br/>체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. + </p> + </div> + <Separator /> + <div> + <TbeTable promises={promises} rfqId={idAsNumber}/> + </div> + </div> + ) +}
\ No newline at end of file diff --git a/app/[lng]/evcp/budgetary/page.tsx b/app/[lng]/evcp/(evcp)/budgetary/page.tsx index 04550353..04550353 100644 --- a/app/[lng]/evcp/budgetary/page.tsx +++ b/app/[lng]/evcp/(evcp)/budgetary/page.tsx diff --git a/app/[lng]/evcp/equip-class/page.tsx b/app/[lng]/evcp/(evcp)/equip-class/page.tsx index 375eb69e..375eb69e 100644 --- a/app/[lng]/evcp/equip-class/page.tsx +++ b/app/[lng]/evcp/(evcp)/equip-class/page.tsx diff --git a/app/[lng]/evcp/faq/manage/actions.ts b/app/[lng]/evcp/(evcp)/faq/manage/actions.ts index bc443a8a..bc443a8a 100644 --- a/app/[lng]/evcp/faq/manage/actions.ts +++ b/app/[lng]/evcp/(evcp)/faq/manage/actions.ts diff --git a/app/[lng]/evcp/faq/manage/page.tsx b/app/[lng]/evcp/(evcp)/faq/manage/page.tsx index 011bbfa4..011bbfa4 100644 --- a/app/[lng]/evcp/faq/manage/page.tsx +++ b/app/[lng]/evcp/(evcp)/faq/manage/page.tsx diff --git a/app/[lng]/evcp/faq/page.tsx b/app/[lng]/evcp/(evcp)/faq/page.tsx index 9b62b7e4..9b62b7e4 100644 --- a/app/[lng]/evcp/faq/page.tsx +++ b/app/[lng]/evcp/(evcp)/faq/page.tsx diff --git a/app/[lng]/evcp/form-list/page.tsx b/app/[lng]/evcp/(evcp)/form-list/page.tsx index f96917d6..f96917d6 100644 --- a/app/[lng]/evcp/form-list/page.tsx +++ b/app/[lng]/evcp/(evcp)/form-list/page.tsx diff --git a/app/[lng]/evcp/items/page.tsx b/app/[lng]/evcp/(evcp)/items/page.tsx index 144689ff..144689ff 100644 --- a/app/[lng]/evcp/items/page.tsx +++ b/app/[lng]/evcp/(evcp)/items/page.tsx diff --git a/app/[lng]/evcp/layout.tsx b/app/[lng]/evcp/(evcp)/layout.tsx index 9dc39f7b..9dc39f7b 100644 --- a/app/[lng]/evcp/layout.tsx +++ b/app/[lng]/evcp/(evcp)/layout.tsx diff --git a/app/[lng]/evcp/po/page.tsx b/app/[lng]/evcp/(evcp)/po/page.tsx index fa528df0..fa528df0 100644 --- a/app/[lng]/evcp/po/page.tsx +++ b/app/[lng]/evcp/(evcp)/po/page.tsx diff --git a/app/[lng]/evcp/poa/page.tsx b/app/[lng]/evcp/(evcp)/poa/page.tsx index dec5e05b..dec5e05b 100644 --- a/app/[lng]/evcp/poa/page.tsx +++ b/app/[lng]/evcp/(evcp)/poa/page.tsx diff --git a/app/[lng]/evcp/pq-criteria/[id]/page.tsx b/app/[lng]/evcp/(evcp)/pq-criteria/[id]/page.tsx index f040a0ca..f040a0ca 100644 --- a/app/[lng]/evcp/pq-criteria/[id]/page.tsx +++ b/app/[lng]/evcp/(evcp)/pq-criteria/[id]/page.tsx diff --git a/app/[lng]/evcp/pq-criteria/page.tsx b/app/[lng]/evcp/(evcp)/pq-criteria/page.tsx index 778baa93..778baa93 100644 --- a/app/[lng]/evcp/pq-criteria/page.tsx +++ b/app/[lng]/evcp/(evcp)/pq-criteria/page.tsx diff --git a/app/[lng]/evcp/pq/[vendorId]/page.tsx b/app/[lng]/evcp/(evcp)/pq/[vendorId]/page.tsx index 97c9a29a..4c2555a3 100644 --- a/app/[lng]/evcp/pq/[vendorId]/page.tsx +++ b/app/[lng]/evcp/(evcp)/pq/[vendorId]/page.tsx @@ -1,8 +1,7 @@ import * as React from "react" import { Shell } from "@/components/shell" -import { Skeleton } from "@/components/ui/skeleton" import { type SearchParams } from "@/types/table" -import { getPQDataByVendorId, getVendorPQsList } from "@/lib/pq/service" +import { getPQDataByVendorId, getVendorPQsList, loadGeneralPQData, loadProjectPQData } from "@/lib/pq/service" import { Vendor } from "@/db/schema/vendors" import { findVendorById } from "@/lib/vendors/service" import VendorPQAdminReview from "@/components/pq/pq-review-detail" @@ -78,7 +77,7 @@ export default async function PQReviewPage(props: IndexPageProps) { data={activeProjectId ? [] : pqData} vendor={vendor} projectId={undefined} - loadData={() => getPQDataByVendorId(vendorId)} + loadData={loadGeneralPQData} pqType="general" /> </TabsContent> @@ -93,8 +92,7 @@ export default async function PQReviewPage(props: IndexPageProps) { projectId={project.projectId} projectName={project.projectName} projectStatus={project.status} - loadData={() => getPQDataByVendorId(vendorId, project.projectId)} - pqType="project" + loadData={(vendorId, _projectId) => loadProjectPQData(vendorId, project.projectId)} pqType="project" /> </TabsContent> ))} diff --git a/app/[lng]/evcp/pq/page.tsx b/app/[lng]/evcp/(evcp)/pq/page.tsx index 46b22b12..46b22b12 100644 --- a/app/[lng]/evcp/pq/page.tsx +++ b/app/[lng]/evcp/(evcp)/pq/page.tsx diff --git a/app/[lng]/evcp/(evcp)/projects/page.tsx b/app/[lng]/evcp/(evcp)/projects/page.tsx new file mode 100644 index 00000000..0320f259 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/projects/page.tsx @@ -0,0 +1,75 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { ItemsTable } from "@/lib/items/table/items-table" +import { getProjectLists } from "@/lib/projects/service" +import { ProjectsTable } from "@/lib/projects/table/projects-table" +import { searchParamsProjectsCache } from "@/lib/projects/validation" + + +interface IndexPageProps { + searchParams: Promise<SearchParams> +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsProjectsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getProjectLists({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + <Shell className="gap-2"> + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center justify-between space-y-2"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + Project List from S-EDP + </h2> + <p className="text-muted-foreground"> + S-EDP로부터 수신하는 프로젝트 리스트입니다. 향후 MDG로 전환됩니다.{" "} + {/* <span className="inline-flex items-center whitespace-nowrap"> + <Ellipsis className="size-3" /> + <span className="ml-1">버튼</span> + </span> + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} + </p> + </div> + </div> + </div> + + <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> + {/* <DateRangePicker + triggerSize="sm" + triggerClassName="ml-auto w-56 sm:w-60" + align="end" + shallow={false} + /> */} + </React.Suspense> + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={6} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} + shrinkZero + /> + } + > + <ProjectsTable promises={promises} /> + </React.Suspense> + </Shell> + ) +} diff --git a/app/[lng]/evcp/report/page.tsx b/app/[lng]/evcp/(evcp)/report/page.tsx index a1e9f8be..a1e9f8be 100644 --- a/app/[lng]/evcp/report/page.tsx +++ b/app/[lng]/evcp/(evcp)/report/page.tsx diff --git a/app/[lng]/evcp/rfq/[id]/cbe/page.tsx b/app/[lng]/evcp/(evcp)/rfq/[id]/cbe/page.tsx index bc32641f..bc32641f 100644 --- a/app/[lng]/evcp/rfq/[id]/cbe/page.tsx +++ b/app/[lng]/evcp/(evcp)/rfq/[id]/cbe/page.tsx diff --git a/app/[lng]/evcp/rfq/[id]/layout.tsx b/app/[lng]/evcp/(evcp)/rfq/[id]/layout.tsx index 2aac90eb..2aac90eb 100644 --- a/app/[lng]/evcp/rfq/[id]/layout.tsx +++ b/app/[lng]/evcp/(evcp)/rfq/[id]/layout.tsx diff --git a/app/[lng]/evcp/rfq/[id]/page.tsx b/app/[lng]/evcp/(evcp)/rfq/[id]/page.tsx index 026ca5ac..026ca5ac 100644 --- a/app/[lng]/evcp/rfq/[id]/page.tsx +++ b/app/[lng]/evcp/(evcp)/rfq/[id]/page.tsx diff --git a/app/[lng]/evcp/rfq/[id]/tbe/page.tsx b/app/[lng]/evcp/(evcp)/rfq/[id]/tbe/page.tsx index 15c5d93c..15c5d93c 100644 --- a/app/[lng]/evcp/rfq/[id]/tbe/page.tsx +++ b/app/[lng]/evcp/(evcp)/rfq/[id]/tbe/page.tsx diff --git a/app/[lng]/evcp/rfq/page.tsx b/app/[lng]/evcp/(evcp)/rfq/page.tsx index 3417b0bf..3417b0bf 100644 --- a/app/[lng]/evcp/rfq/page.tsx +++ b/app/[lng]/evcp/(evcp)/rfq/page.tsx diff --git a/app/[lng]/evcp/settings/layout.tsx b/app/[lng]/evcp/(evcp)/settings/layout.tsx index 6f373567..6f373567 100644 --- a/app/[lng]/evcp/settings/layout.tsx +++ b/app/[lng]/evcp/(evcp)/settings/layout.tsx diff --git a/app/[lng]/evcp/settings/page.tsx b/app/[lng]/evcp/(evcp)/settings/page.tsx index a6eaac90..a6eaac90 100644 --- a/app/[lng]/evcp/settings/page.tsx +++ b/app/[lng]/evcp/(evcp)/settings/page.tsx diff --git a/app/[lng]/evcp/settings/preferences/page.tsx b/app/[lng]/evcp/(evcp)/settings/preferences/page.tsx index e2a88021..e2a88021 100644 --- a/app/[lng]/evcp/settings/preferences/page.tsx +++ b/app/[lng]/evcp/(evcp)/settings/preferences/page.tsx diff --git a/app/[lng]/evcp/system/admin-users/page.tsx b/app/[lng]/evcp/(evcp)/system/admin-users/page.tsx index 11a9e9fb..11a9e9fb 100644 --- a/app/[lng]/evcp/system/admin-users/page.tsx +++ b/app/[lng]/evcp/(evcp)/system/admin-users/page.tsx diff --git a/app/[lng]/evcp/system/layout.tsx b/app/[lng]/evcp/(evcp)/system/layout.tsx index 4885a028..62f3e845 100644 --- a/app/[lng]/evcp/system/layout.tsx +++ b/app/[lng]/evcp/(evcp)/system/layout.tsx @@ -28,7 +28,7 @@ export default async function SettingsLayout({ const sidebarNavItems = [ { - title: "Users", + title: "SHI Users", href: `/${lng}/evcp/system`, }, { diff --git a/app/[lng]/evcp/system/page.tsx b/app/[lng]/evcp/(evcp)/system/page.tsx index 2d180028..fe0a262c 100644 --- a/app/[lng]/evcp/system/page.tsx +++ b/app/[lng]/evcp/(evcp)/system/page.tsx @@ -42,7 +42,7 @@ export default async function SystemUserPage(props: IndexPageProps) { > <div className="space-y-6"> <div> - <h3 className="text-lg font-medium">Users</h3> + <h3 className="text-lg font-medium">SHI Users</h3> <p className="text-sm text-muted-foreground"> 시스템 전체 사용자들을 조회하고 관리할 수 있는 페이지입니다. 사용자에게 롤을 할당하는 것으로 메뉴별 권한을 관리할 수 있습니다. </p> diff --git a/app/[lng]/evcp/system/permissions/page.tsx b/app/[lng]/evcp/(evcp)/system/permissions/page.tsx index 6aa2b693..6aa2b693 100644 --- a/app/[lng]/evcp/system/permissions/page.tsx +++ b/app/[lng]/evcp/(evcp)/system/permissions/page.tsx diff --git a/app/[lng]/evcp/system/roles/page.tsx b/app/[lng]/evcp/(evcp)/system/roles/page.tsx index fe074600..fe074600 100644 --- a/app/[lng]/evcp/system/roles/page.tsx +++ b/app/[lng]/evcp/(evcp)/system/roles/page.tsx diff --git a/app/[lng]/evcp/tag-numbering/page.tsx b/app/[lng]/evcp/(evcp)/tag-numbering/page.tsx index 9d5b903a..9d5b903a 100644 --- a/app/[lng]/evcp/tag-numbering/page.tsx +++ b/app/[lng]/evcp/(evcp)/tag-numbering/page.tsx diff --git a/app/[lng]/evcp/tasks/page.tsx b/app/[lng]/evcp/(evcp)/tasks/page.tsx index f14cc757..f14cc757 100644 --- a/app/[lng]/evcp/tasks/page.tsx +++ b/app/[lng]/evcp/(evcp)/tasks/page.tsx diff --git a/app/[lng]/evcp/(evcp)/tbe/page.tsx b/app/[lng]/evcp/(evcp)/tbe/page.tsx new file mode 100644 index 00000000..2461ed42 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/tbe/page.tsx @@ -0,0 +1,113 @@ +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getAllTBE } from "@/lib/rfqs/service" +import { searchParamsTBECache } from "@/lib/rfqs/validations" +import { AllTbeTable } from "@/lib/tbe/table/tbe-table" +import { RfqType } from "@/lib/rfqs/validations" +import * as React from "react" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" + +interface IndexPageProps { + params: { + lng: string + } + searchParams: Promise<SearchParams> +} + +// 타입별 페이지 설명 구성 (Budgetary 제외) +const typeConfig: Record<string, { title: string; description: string; rfqType: RfqType }> = { + "purchase": { + title: "Purchase RFQ Technical Bid Evaluation", + description: "실제 구매 발주 전 가격 요청을 위한 TBE입니다.", + rfqType: RfqType.PURCHASE + }, + "purchase-budgetary": { + title: "Purchase Budgetary RFQ Technical Bid Evaluation", + description: "프로젝트 수주 후, 공식 입찰 전 예산 책정을 위한 TBE입니다.", + rfqType: RfqType.PURCHASE_BUDGETARY + } +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + + // URL 쿼리 파라미터에서 타입 추출 + const searchParams = await props.searchParams + // 기본값으로 'purchase' 사용 + const typeParam = searchParams?.type as string || 'purchase' + + // 유효한 타입인지 확인하고 기본값 설정 + const validType = Object.keys(typeConfig).includes(typeParam) ? typeParam : 'purchase' + const rfqType = typeConfig[validType].rfqType + + // SearchParams 파싱 (Zod) + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // 현재 선택된 타입의 데이터 로드 + const promises = Promise.all([ + getAllTBE({ + ...search, + filters: validFilters, + rfqType + }) + ]) + + // 페이지 경로 생성 함수 - 단순화 + const getTabUrl = (type: string) => { + return `/${lng}/evcp/tbe?type=${type}`; + } + + return ( + <Shell className="gap-2"> + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center justify-between space-y-2"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + Technical Bid Evaluation + </h2> + <p className="text-muted-foreground"> + 초대된 벤더에게 TBE를 보낼 수 있습니다. <br/> + 체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. + </p> + </div> + </div> + </div> + + {/* 타입 선택 탭 (Budgetary 제외) */} + <Tabs defaultValue={validType} value={validType} className="w-full"> + <TabsList className="grid grid-cols-2 w-full max-w-md"> + <TabsTrigger value="purchase" asChild> + <a href={getTabUrl('purchase')}>Purchase</a> + </TabsTrigger> + <TabsTrigger value="purchase-budgetary" asChild> + <a href={getTabUrl('purchase-budgetary')}>Purchase Budgetary</a> + </TabsTrigger> + </TabsList> + + <div className="mt-2"> + <p className="text-sm text-muted-foreground"> + {typeConfig[validType].description} + </p> + </div> + </Tabs> + + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={6} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} + shrinkZero + /> + } + > + <AllTbeTable promises={promises}/> + </React.Suspense> + </Shell> + ) +}
\ No newline at end of file diff --git a/app/[lng]/evcp/vendor-candidates/page.tsx b/app/[lng]/evcp/(evcp)/vendor-candidates/page.tsx index 668c0dc6..668c0dc6 100644 --- a/app/[lng]/evcp/vendor-candidates/page.tsx +++ b/app/[lng]/evcp/(evcp)/vendor-candidates/page.tsx diff --git a/app/[lng]/evcp/vendor-investigation/page.tsx b/app/[lng]/evcp/(evcp)/vendor-investigation/page.tsx index c59de869..c59de869 100644 --- a/app/[lng]/evcp/vendor-investigation/page.tsx +++ b/app/[lng]/evcp/(evcp)/vendor-investigation/page.tsx diff --git a/app/[lng]/evcp/vendors/[id]/info/items/page.tsx b/app/[lng]/evcp/(evcp)/vendors/[id]/info/items/page.tsx index e9ff17b4..e9ff17b4 100644 --- a/app/[lng]/evcp/vendors/[id]/info/items/page.tsx +++ b/app/[lng]/evcp/(evcp)/vendors/[id]/info/items/page.tsx diff --git a/app/[lng]/evcp/vendors/[id]/info/layout.tsx b/app/[lng]/evcp/(evcp)/vendors/[id]/info/layout.tsx index 39e0bac0..39e0bac0 100644 --- a/app/[lng]/evcp/vendors/[id]/info/layout.tsx +++ b/app/[lng]/evcp/(evcp)/vendors/[id]/info/layout.tsx diff --git a/app/[lng]/evcp/vendors/[id]/info/page.tsx b/app/[lng]/evcp/(evcp)/vendors/[id]/info/page.tsx index 6279e924..6279e924 100644 --- a/app/[lng]/evcp/vendors/[id]/info/page.tsx +++ b/app/[lng]/evcp/(evcp)/vendors/[id]/info/page.tsx diff --git a/app/[lng]/evcp/vendors/[id]/info/rfq-history/page.tsx b/app/[lng]/evcp/(evcp)/vendors/[id]/info/rfq-history/page.tsx index 1d2f618c..1d2f618c 100644 --- a/app/[lng]/evcp/vendors/[id]/info/rfq-history/page.tsx +++ b/app/[lng]/evcp/(evcp)/vendors/[id]/info/rfq-history/page.tsx diff --git a/app/[lng]/evcp/vendors/page.tsx b/app/[lng]/evcp/(evcp)/vendors/page.tsx index e3cc7fdc..e3cc7fdc 100644 --- a/app/[lng]/evcp/vendors/page.tsx +++ b/app/[lng]/evcp/(evcp)/vendors/page.tsx diff --git a/app/[lng]/evcp/page.tsx b/app/[lng]/evcp/page.tsx index a1e9f8be..f9662cb7 100644 --- a/app/[lng]/evcp/page.tsx +++ b/app/[lng]/evcp/page.tsx @@ -1,8 +1,21 @@ +import { Metadata } from "next" +import { Suspense } from "react" +import { LoginFormSkeleton } from "@/components/login/login-form-skeleton" +import { LoginFormSHI } from "@/components/login/login-form-shi" -export default function Pages() { - return ( - <> - test - </> - ) - }
\ No newline at end of file +export const metadata: Metadata = { + title: "eVCP Portal", + description: "", +} + +export default function AuthenticationPage() { + + + return ( + <> + <Suspense fallback={<LoginFormSkeleton/>}> + <LoginFormSHI /> + </Suspense> + </> + ) +} diff --git a/app/[lng]/partners/pq/page.tsx b/app/[lng]/partners/pq/page.tsx index 42c88b21..08faeebb 100644 --- a/app/[lng]/partners/pq/page.tsx +++ b/app/[lng]/partners/pq/page.tsx @@ -1,85 +1,44 @@ import { getServerSession } from "next-auth" import { authOptions } from "@/app/api/auth/[...nextauth]/route" -import * as React from "react" -import { Shell } from "@/components/shell" -import { Skeleton } from "@/components/ui/skeleton" import { getPQDataByVendorId, getPQProjectsByVendorId } from "@/lib/pq/service" -import { PQInputTabs } from "@/components/pq/pq-input-tabs" -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { ClientPQWrapper } from "@/components/pq/client-pq-input-wrapper" +import { unstable_noStore as noStore } from 'next/cache' +// 페이지가 기본적으로 동적임을 나타냄 +export const dynamic = "force-dynamic" export default async function PQInputPage({ - searchParams + searchParams, }: { searchParams: { projectId?: string } }) { + // Opt out of caching for this route + noStore() + // 세션 const session = await getServerSession(authOptions) // 예: 세션에서 vendorId 가져오기 // const vendorId = session?.user.companyId const vendorId = 17 // 임시 const idAsNumber = Number(vendorId) - - const projectId = searchParams.projectId ? parseInt(searchParams.projectId, 10) : undefined - - // 벤더에게 요청된 프로젝트 PQ 목록 가져오기 (탭 표시용) + + // 서버에서는 모든 데이터를 가져오고, 프로젝트 필터링은 클라이언트에서 진행 const projectPQs = await getPQProjectsByVendorId(idAsNumber) - - // PQ 데이터 조회 - const pqData = await getPQDataByVendorId(idAsNumber, projectId) - - // 현재 프로젝트 정보 (있다면) - const currentProject = projectId - ? projectPQs.find(p => p.projectId === projectId) - : null - + + // 두 가지 방법으로 수정할 수 있습니다: + + // 방법 1: 먼저 allPQData 데이터를 projectId 없이 가져오기 + const allPQData = await getPQDataByVendorId(idAsNumber, undefined) + + // 방법 2: rawProjectId를 클라이언트로 전달하고, 클라이언트가 필터링을 처리 + + // 클라이언트 컴포넌트로 데이터와 원시 searchParams 전달 return ( - <Shell className="gap-2"> - {/* 헤더 - 프로젝트 정보 포함 */} - <div className="space-y-2"> - <h2 className="text-2xl font-bold tracking-tight"> - Pre-Qualification Check Sheet - {currentProject && ( - <span className="ml-2 text-muted-foreground"> - - {currentProject.projectCode} - </span> - )} - </h2> - <p className="text-muted-foreground"> - PQ에 적절한 응답을 제출하시기 바랍니다. - </p> - </div> - - {/* 일반/프로젝트 PQ 선택 탭 */} - {projectPQs.length > 0 && ( - <div className="border-b"> - <Tabs defaultValue={projectId ? `project-${projectId}` : "general"}> - <TabsList> - <TabsTrigger value="general" asChild> - <a href="/partners/pq">일반 PQ</a> - </TabsTrigger> - - {projectPQs.map(project => ( - <TabsTrigger key={project.projectId} value={`project-${project.projectId}`} asChild> - <a href={`/partners/pq?projectId=${project.projectId}`}> - {project.projectCode} - </a> - </TabsTrigger> - ))} - </TabsList> - </Tabs> - </div> - )} - - {/* PQ 입력 탭 */} - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - <PQInputTabs - data={pqData} - vendorId={idAsNumber} - projectId={projectId} - projectData={currentProject} - /> - </React.Suspense> - </Shell> + <ClientPQWrapper + allPQData={allPQData} + projectPQs={projectPQs} + vendorId={idAsNumber} + rawSearchParams={searchParams} + /> ) }
\ No newline at end of file |
